In [5]:
import scipy.integrate
import scipy.sparse
import numpy as np
import collections
import matplotlib.pyplot as plt
%matplotlib inline

class IVPResult:
    pass

def solve_ivp(f, ts, x0, p=None, integrator='dopri5', store_trajectory=False,
              sens=False, dfx=None, dfp=None):
    """
    Solve initial value problem
        d/dt x = f(t, x, p);  x(t0) = x0.

    Evaluate Solution at time points specified in ts, with ts[0] = t0.

    Compute sensitivity matrices evaluated at time points in ts if sens=True.
    """

    if sens:
        def f_vode(t, xGxGp, p):
            x     = xGxGp[:x0.shape[0]]
            Gx    = xGxGp[x0.shape[0]:x0.shape[0]+x0.shape[0]*x0.shape[0]]
            Gx    = Gx.reshape([x0.shape[0], x0.shape[0]])
            Gp    = xGxGp[x0.shape[0]+x0.shape[0]*x0.shape[0]:]
            Gp    = Gp.reshape([x0.shape[0], p.shape[0]])
            dx    = f(t, x, p)
            dfxev = dfx(t, x, p)
            dfpev = dfp(t, x, p)
            dGx   = dfxev.dot(Gx)
            dGp   = dfxev.dot(Gp) + dfpev
            return np.concatenate(
                [dx,
                 dGx.reshape(-1),
                 dGp.reshape(-1)])

        ivp = scipy.integrate.ode(f_vode)
    else:
        ivp = scipy.integrate.ode(f)

    ivp.set_integrator(integrator)

    if store_trajectory:
        times = []
        points = []
        def solout(t, x):
            if len(times) == 0 or t != times[-1]:
                times.append(t)
                points.append(np.copy(x[:x0.shape[0]]))
        ivp.set_solout(solout)

    if sens:
        ivp.set_initial_value(np.concatenate(
            [x0,
             np.eye(x0.shape[0]).reshape(-1),
             np.zeros([x0.shape[0], p.shape[0]]).reshape(-1)
            ]), ts[0])
    else:
        ivp.set_initial_value(x0, ts[0])
    ivp.set_f_params(p)

    result = IVPResult()
    result.ts = ts
    result.xs  = np.zeros([ts.shape[0], x0.shape[0]])
    if sens:
        result.Gxs = np.zeros([ts.shape[0], x0.shape[0], x0.shape[0]])
        result.Gps = np.zeros([ts.shape[0], x0.shape[0], p.shape[0]])
    result.success = True

    result.xs[0,:] = x0
    for ii in range(1,ts.shape[0]):
        ivp.integrate(ts[ii])
        result.xs[ii,:]    = ivp.y[:x0.shape[0]]
        if sens:
            result.Gxs[ii,:,:] = ivp.y[x0.shape[0]:x0.shape[0]+x0.shape[0]*x0.shape[0]].reshape([x0.shape[0], x0.shape[0]])
            result.Gps[ii,:,:] = ivp.y[x0.shape[0]+x0.shape[0]*x0.shape[0]:].reshape([x0.shape[0], p.shape[0]])
        if not ivp.successful():
            result.success = False
            break

    if store_trajectory:
        result.trajectory_t = np.array(times)
        result.trajectory_x = np.array(points)

    return result


# delta x should be a vector of dimension equal to the number
# of variables, which is true only if we do not change the parameters.
def evaluate_parest_single_shooting(f, dfx, dfp, meas_times, meas_values, s0, p):
    result  = solve_ivp(f, meas_times, s0, p=p, sens=True, dfx=dfx, dfp=dfp)
    m = len(meas_times)
    nx = len(s0)
    nv = nx + len(p)
    # make F a column vector with [y11, y12, y21, y22, y31, y32, ... ym1, ym2]
    F = (meas_values - result.xs).reshape((m * nx, 1))
    # each row of J will be [dFij / dx01, dFij / dx02, dFij / dp1, ..., dFij / dp6 ]
    J = np.zeros((m * nx, nv))
    dy = np.eye(nx)
    for i in range(m):
        for j in range(nx):
            # dhi / dy = dyi / dy = all zeros except for the ith element
            # that will be 1
            # For dFij / dp I  dhi / dp = dyi(tj, yi, p) / dp is equal to 0
            J[2 * i + j, :] = np.hstack((dy[[j], :].dot(result.Gxs[i]), dy[[j], :].dot(result.Gps[i])))
    return F, J

In [2]:
def dX_dt(t, X, p): #predator-prey DEs
    R = X[0]
    F = X[1]
    alpha, beta, gamma, delta = p
    dRdt = alpha * R - beta * F * R
    dFdt = gamma * R * F - delta * F
    return np.array([dRdt,dFdt])

def dfx(t, X, p):

    R = X[0]
    F = X[1]
    alpha, beta, gamma, delta = p
    return np.array([alpha - beta * F, - beta * R, gamma * F,\
                            gamma * R -  delta]).reshape((2, 2))

def dfp(t, X, p):
    R = X[0]
    F = X[1]
    alpha, beta, gamma, delta = p

    return np.array([[R, - F * R, 0., 0.],[0., 0.,  R * F, -F]])



X0 = np.array([20.,10.])
param = np.array((.3, .01, .001, .1))
result = solve_ivp(dX_dt, np.array([0, 300]), X0, p=param,\
                   integrator='dopri5', store_trajectory=True, sens=True, dfx=dfx, dfp=dfp)

In [3]:
def gauss_newton(F_J, x0, meas_times, meas_values, itmax=100, tol=1e-7, verbose=1):
    
    s0 = x0[:2]
    param = x0[2:]
    tau = .5
    if verbose == 1:
        print("Start gauss_newton with itmax=",itmax,"and tol=",tol,"and tau=",tau)
        Fs = np.zeros(itmax+1)
    i = 0
    norm = tol+1.0
    while i<itmax and norm > tol:
        F, J= F_J(dX_dt, dfx, dfp, meas_times, meas_values, s0, param)
        Dx = np.linalg.lstsq(J, F)[0]
        s0 += tau * Dx.flatten()[:2]
        param += tau * Dx.flatten()[2:]
        norm = np.linalg.norm(Dx.flatten())
        if verbose == 1:
            Fs[i] = (np.linalg.norm(F))
            print("It.step: ",i+1, '\t', u'\u2016Dx\u2016\u2082 =', norm)
        i += 1
        
            
    if verbose == 1:
        print("\n The shooting residual F throughout the iterations:")
        plt.plot(Fs[:i])
        Fs
        
    return s0, param

In [4]:
np.random.seed(31)
meas_times = np.arange(21) * 5
noise = 5. * np.random.rand(21, 2)
s0_init = np.array([20., 10.])
param_init = np.array((.2, .01, .001, .1))
result = solve_ivp(dX_dt, meas_times, s0_init, p=param_init)
meas_values = result.xs + noise

print("Initial values close to the correct parameters:")
# We start with an alpha variation ftom the true values 
alpha = .01
s0 = s0_init * (1 + alpha * np.random.rand(len(s0_init)))
param = param_init * (1 + alpha * np.random.rand(len(param_init.shape))) 

s0, param = gauss_newton(evaluate_parest_single_shooting, np.concatenate([s0,param]),meas_times,meas_values)
print('relative  error')
print((param - param_init) / param_init)
print((s0 - s0_init)/ s0_init, '\n')
print('estimated parameter vs true ones')
print(param)
print(param_init)


Initial values close to the correct parameters:
Start gauss_newton with itmax= 100 and tol= 1e-07 and tau= 0.5
It.step:  1 	 ‖Dx‖₂ = 1.15089430048
It.step:  2 	 ‖Dx‖₂ = 0.491694548705
It.step:  3 	 ‖Dx‖₂ = 0.241385738148
It.step:  4 	 ‖Dx‖₂ = 0.127822344162
It.step:  5 	 ‖Dx‖₂ = 0.0698792869653
It.step:  6 	 ‖Dx‖₂ = 0.038614067321
It.step:  7 	 ‖Dx‖₂ = 0.021376214158
It.step:  8 	 ‖Dx‖₂ = 0.0118129341029
It.step:  9 	 ‖Dx‖₂ = 0.00650794460314
It.step:  10 	 ‖Dx‖₂ = 0.00357281325268
It.step:  11 	 ‖Dx‖₂ = 0.00195457186905
It.step:  12 	 ‖Dx‖₂ = 0.00106570457362
It.step:  13 	 ‖Dx‖₂ = 0.000579249010345
It.step:  14 	 ‖Dx‖₂ = 0.000313939715172
It.step:  15 	 ‖Dx‖₂ = 0.000169702692461
It.step:  16 	 ‖Dx‖₂ = 9.15159466658e-05
It.step:  17 	 ‖Dx‖₂ = 4.92455859761e-05
It.step:  18 	 ‖Dx‖₂ = 2.64477863353e-05
It.step:  19 	 ‖Dx‖₂ = 1.41789418997e-05
It.step:  20 	 ‖Dx‖₂ = 7.58934244085e-06
It.step:  21 	 ‖Dx‖₂ = 4.05636001627e-06
It.step:  22 	 ‖Dx‖₂ = 2.16521219033e-06
It.step:  23 	 ‖Dx‖₂ = 1.15438276524e-06
It.step:  24 	 ‖Dx‖₂ = 6.14798481636e-07
It.step:  25 	 ‖Dx‖₂ = 3.27109235587e-07
It.step:  26 	 ‖Dx‖₂ = 1.73887948331e-07
It.step:  27 	 ‖Dx‖₂ = 9.23629127731e-08

 The shooting residual F throughout the iterations:
relative  error
[ 0.00698013 -0.05993932 -0.03122551 -0.01620582]
[ 0.03716307  0.09653196] 

estimated parameter vs true ones
[ 0.20139603  0.00940061  0.00096877  0.09837942]
[ 0.2    0.01   0.001  0.1  ]

In [5]:
print("Initial values still close to the correct parameters, but already needing more iterations:")
s0 = np.array([22,11])
param = np.array([0.21,0.011,0.0011,0.11])

s0, param = gauss_newton(evaluate_parest_single_shooting, np.concatenate([s0,param]),meas_times,meas_values)

print('relative  error')
print((param - param_init) / param_init)
print((s0 - s0_init)/ s0_init, '\n')
print('estimated parameter vs true ones')
print(param)
print(param_init)


Initial values still close to the correct parameters, but already needing more iterations:
Start gauss_newton with itmax= 100 and tol= 1e-07 and tau= 0.5
It.step:  1 	 ‖Dx‖₂ = 17.4863785956
It.step:  2 	 ‖Dx‖₂ = 19.9914658061
It.step:  3 	 ‖Dx‖₂ = 8.30403039778
It.step:  4 	 ‖Dx‖₂ = 16.5112014218
It.step:  5 	 ‖Dx‖₂ = 12.1936262638
It.step:  6 	 ‖Dx‖₂ = 7.73396976672
It.step:  7 	 ‖Dx‖₂ = 1.97215333528
It.step:  8 	 ‖Dx‖₂ = 2.98215116873
It.step:  9 	 ‖Dx‖₂ = 1.79992718126
It.step:  10 	 ‖Dx‖₂ = 1.10740550362
It.step:  11 	 ‖Dx‖₂ = 0.659427539923
It.step:  12 	 ‖Dx‖₂ = 0.382108301801
It.step:  13 	 ‖Dx‖₂ = 0.217708591172
It.step:  14 	 ‖Dx‖₂ = 0.122955427265
It.step:  15 	 ‖Dx‖₂ = 0.0691018563608
It.step:  16 	 ‖Dx‖₂ = 0.0386858660943
It.step:  17 	 ‖Dx‖₂ = 0.0215722701802
It.step:  18 	 ‖Dx‖₂ = 0.0119787007152
It.step:  19 	 ‖Dx‖₂ = 0.00662298341116
It.step:  20 	 ‖Dx‖₂ = 0.0036464650547
It.step:  21 	 ‖Dx‖₂ = 0.00199970665157
It.step:  22 	 ‖Dx‖₂ = 0.0010926153413
It.step:  23 	 ‖Dx‖₂ = 0.000594998236668
It.step:  24 	 ‖Dx‖₂ = 0.000323035256187
It.step:  25 	 ‖Dx‖₂ = 0.000174904189001
It.step:  26 	 ‖Dx‖₂ = 9.44682246661e-05
It.step:  27 	 ‖Dx‖₂ = 5.09112907982e-05
It.step:  28 	 ‖Dx‖₂ = 2.7383030742e-05
It.step:  29 	 ‖Dx‖₂ = 1.47019086719e-05
It.step:  30 	 ‖Dx‖₂ = 7.88074035396e-06
It.step:  31 	 ‖Dx‖₂ = 4.21821988168e-06
It.step:  32 	 ‖Dx‖₂ = 2.25486575875e-06
It.step:  33 	 ‖Dx‖₂ = 1.20391346193e-06
It.step:  34 	 ‖Dx‖₂ = 6.42097325257e-07
It.step:  35 	 ‖Dx‖₂ = 3.42121711683e-07
It.step:  36 	 ‖Dx‖₂ = 1.82126254866e-07
It.step:  37 	 ‖Dx‖₂ = 9.68749950721e-08

 The shooting residual F throughout the iterations:
relative  error
[ 0.00698014 -0.05993932 -0.03122552 -0.01620583]
[ 0.03716307  0.09653197] 

estimated parameter vs true ones
[ 0.20139603  0.00940061  0.00096877  0.09837942]
[ 0.2    0.01   0.001  0.1  ]

In [6]:
print("Initial values such that the method fails:")
s0 = np.array([22,11])
param = np.array([0.23,0.011,0.0011,0.11])

s0, param = gauss_newton(evaluate_parest_single_shooting, np.concatenate([s0,param]),meas_times,meas_values)

print('relative  error')
print((param - param_init) / param_init)
print((s0 - s0_init)/ s0_init, '\n')
print('estimated parameter vs true ones')
print(param)
print(param_init)


Initial values such that the method fails:
Start gauss_newton with itmax= 100 and tol= 1e-07 and tau= 0.5
It.step:  1 	 ‖Dx‖₂ = 24.1375174457
It.step:  2 	 ‖Dx‖₂ = 51.1793785165
It.step:  3 	 ‖Dx‖₂ = 69.1296919606
D:\Users\Ihno\Anaconda3\lib\site-packages\scipy\integrate\_ode.py:1035: UserWarning: dopri5: larger nmax is needed
  self.messages.get(idid, 'Unexpected idid=%s' % idid))
It.step:  4 	 ‖Dx‖₂ = 10693.5375497
It.step:  5 	 ‖Dx‖₂ = 0.161181144104
It.step:  6 	 ‖Dx‖₂ = 0.078168139076
It.step:  7 	 ‖Dx‖₂ = 0.0341117480226
It.step:  8 	 ‖Dx‖₂ = 4.47883484782e+12
It.step:  9 	 ‖Dx‖₂ = 1154023923.46
It.step:  10 	 ‖Dx‖₂ = 3.06385760693e-06
It.step:  11 	 ‖Dx‖₂ = 3.06385776921e-06
It.step:  12 	 ‖Dx‖₂ = 3.0638572972e-06
D:\Users\Ihno\Anaconda3\lib\site-packages\scipy\integrate\_ode.py:1035: UserWarning: dopri5: step size becomes too small
  self.messages.get(idid, 'Unexpected idid=%s' % idid))
It.step:  13 	 ‖Dx‖₂ = 3.06385758492e-06
It.step:  14 	 ‖Dx‖₂ = 3.06385767151e-06
It.step:  15 	 ‖Dx‖₂ = 3.06385787241e-06
It.step:  16 	 ‖Dx‖₂ = 3.06385751957e-06
It.step:  17 	 ‖Dx‖₂ = 3.06385745747e-06
It.step:  18 	 ‖Dx‖₂ = 3.06385761359e-06
It.step:  19 	 ‖Dx‖₂ = 3.0638578102e-06
It.step:  20 	 ‖Dx‖₂ = 3.06385799133e-06
It.step:  21 	 ‖Dx‖₂ = 3.06385776816e-06
It.step:  22 	 ‖Dx‖₂ = 3.06385771533e-06
It.step:  23 	 ‖Dx‖₂ = 3.06385769637e-06
It.step:  24 	 ‖Dx‖₂ = 3.06385793027e-06
It.step:  25 	 ‖Dx‖₂ = 3.06385831492e-06
It.step:  26 	 ‖Dx‖₂ = 3.06385824628e-06
It.step:  27 	 ‖Dx‖₂ = 3.06385817332e-06
It.step:  28 	 ‖Dx‖₂ = 3.06385824061e-06
It.step:  29 	 ‖Dx‖₂ = 3.06385782554e-06
It.step:  30 	 ‖Dx‖₂ = 3.06385788285e-06
It.step:  31 	 ‖Dx‖₂ = 3.06385785722e-06
It.step:  32 	 ‖Dx‖₂ = 3.06385761278e-06
It.step:  33 	 ‖Dx‖₂ = 3.06385795038e-06
It.step:  34 	 ‖Dx‖₂ = 3.06385796507e-06
It.step:  35 	 ‖Dx‖₂ = 3.06385757068e-06
It.step:  36 	 ‖Dx‖₂ = 3.06385786509e-06
It.step:  37 	 ‖Dx‖₂ = 3.06385765089e-06
It.step:  38 	 ‖Dx‖₂ = 3.06385832853e-06
It.step:  39 	 ‖Dx‖₂ = 3.06385739409e-06
It.step:  40 	 ‖Dx‖₂ = 3.06385736711e-06
It.step:  41 	 ‖Dx‖₂ = 3.06385742907e-06
It.step:  42 	 ‖Dx‖₂ = 3.06385718304e-06
It.step:  43 	 ‖Dx‖₂ = 3.06385728449e-06
It.step:  44 	 ‖Dx‖₂ = 3.06385746633e-06
It.step:  45 	 ‖Dx‖₂ = 3.06385753448e-06
It.step:  46 	 ‖Dx‖₂ = 3.06385724114e-06
It.step:  47 	 ‖Dx‖₂ = 3.06385724978e-06
It.step:  48 	 ‖Dx‖₂ = 3.06385744564e-06
It.step:  49 	 ‖Dx‖₂ = 3.06385747647e-06
It.step:  50 	 ‖Dx‖₂ = 3.06385796741e-06
It.step:  51 	 ‖Dx‖₂ = 3.06385745177e-06
It.step:  52 	 ‖Dx‖₂ = 3.06385822285e-06
It.step:  53 	 ‖Dx‖₂ = 3.06385792663e-06
It.step:  54 	 ‖Dx‖₂ = 3.06385804435e-06
It.step:  55 	 ‖Dx‖₂ = 3.06385807122e-06
It.step:  56 	 ‖Dx‖₂ = 3.06385776339e-06
It.step:  57 	 ‖Dx‖₂ = 3.06385815786e-06
It.step:  58 	 ‖Dx‖₂ = 3.06385799292e-06
It.step:  59 	 ‖Dx‖₂ = 3.06385816053e-06
It.step:  60 	 ‖Dx‖₂ = 3.06385788576e-06
It.step:  61 	 ‖Dx‖₂ = 3.06385799383e-06
It.step:  62 	 ‖Dx‖₂ = 3.06385788643e-06
It.step:  63 	 ‖Dx‖₂ = 3.06385757863e-06
It.step:  64 	 ‖Dx‖₂ = 3.06385807059e-06
It.step:  65 	 ‖Dx‖₂ = 3.06385763428e-06
It.step:  66 	 ‖Dx‖₂ = 3.06385784187e-06
It.step:  67 	 ‖Dx‖₂ = 3.0638575999e-06
It.step:  68 	 ‖Dx‖₂ = 3.06385800639e-06
It.step:  69 	 ‖Dx‖₂ = 3.06385782897e-06
It.step:  70 	 ‖Dx‖₂ = 3.06385761089e-06
It.step:  71 	 ‖Dx‖₂ = 3.06385764326e-06
It.step:  72 	 ‖Dx‖₂ = 3.06385764051e-06
It.step:  73 	 ‖Dx‖₂ = 3.06385735572e-06
It.step:  74 	 ‖Dx‖₂ = 3.06385786327e-06
It.step:  75 	 ‖Dx‖₂ = 3.06385776577e-06
It.step:  76 	 ‖Dx‖₂ = 3.06385788633e-06
It.step:  77 	 ‖Dx‖₂ = 3.06385751129e-06
It.step:  78 	 ‖Dx‖₂ = 3.06385766606e-06
It.step:  79 	 ‖Dx‖₂ = 3.0638575577e-06
It.step:  80 	 ‖Dx‖₂ = 3.0638575604e-06
It.step:  81 	 ‖Dx‖₂ = 3.06385732833e-06
It.step:  82 	 ‖Dx‖₂ = 3.06385779912e-06
It.step:  83 	 ‖Dx‖₂ = 3.06385790602e-06
It.step:  84 	 ‖Dx‖₂ = 3.06385788041e-06
It.step:  85 	 ‖Dx‖₂ = 3.06385793007e-06
It.step:  86 	 ‖Dx‖₂ = 3.06385767573e-06
It.step:  87 	 ‖Dx‖₂ = 3.06385814391e-06
It.step:  88 	 ‖Dx‖₂ = 3.06385817491e-06
It.step:  89 	 ‖Dx‖₂ = 3.06385803737e-06
It.step:  90 	 ‖Dx‖₂ = 3.0638580843e-06
It.step:  91 	 ‖Dx‖₂ = 3.06385781996e-06
It.step:  92 	 ‖Dx‖₂ = 3.06385819021e-06
It.step:  93 	 ‖Dx‖₂ = 3.06385793921e-06
It.step:  94 	 ‖Dx‖₂ = 3.0638583956e-06
It.step:  95 	 ‖Dx‖₂ = 3.06385779899e-06
It.step:  96 	 ‖Dx‖₂ = 3.06385776573e-06
It.step:  97 	 ‖Dx‖₂ = 3.06385751731e-06
It.step:  98 	 ‖Dx‖₂ = 3.06385800247e-06
It.step:  99 	 ‖Dx‖₂ = 3.06385778461e-06
It.step:  100 	 ‖Dx‖₂ = 3.06385722311e-06

 The shooting residual F throughout the iterations:
relative  error
[ -2.31365028e+00  -3.05544341e+13  -2.21847048e+15   3.30493623e+09]
[ -2.19189614e+06   4.54737276e+08] 

estimated parameter vs true ones
[ -2.62730057e-01  -3.05544341e+11  -2.21847048e+12   3.30493623e+08]
[ 0.2    0.01   0.001  0.1  ]

We observe that we can vary our initial guess in a small region around the correct parameters, but the region is rather small, as we pretty fast arrive at initial guesses for which the method fails to find a suitable estimation!


In [1]:
from pylab import *

In [7]:
x = np.linspace(-3, 3, 1000)
plt.plot(x, sqrt((cos(x) - sin(x)) **2))
plt.plot(x, sqrt(1 -sin(2*x)))


Out[7]:
[<matplotlib.lines.Line2D at 0x7fa3a22f8f28>]

In [ ]: